#!/usr/bin/env python
#
#  Copyright (c) 1999, 2000, 2001 Sean Reifschneider, tummy.com, ltd.
#  All Rights Reserved

'''Functions for manipulation of net strings.

Netstrings (as described in ftp://koobera.math.uic.edu/www/proto/netstrings.txt)
are strings of the form "<length in bytes>:<string>,".  They allow the
programmer to allocate an appropriately sized buffer ahead of time, and also
to ensure that an entire string has been received (instead of relying on
parsing strings on CR-LF markers).

Simple netstring example:

	from netstring import *

	print 'Doing hello+there "%s"' % str(nstos('5:hello,5:there,', 1))
	print 'Doing empty+there "%s"' % str(nstos('0:,5:there,', 1))
	print 'Creating hello world!: "%s"' % stons('hello world!')
	print 'Iscomplete:', iscomplete('12:hello world')
	print 'Iscomplete:', iscomplete('12:hello world!')
	print 'Iscomplete:', iscomplete('12:hello world!,')
	print 'Iscomplete:', iscomplete('12:hello world!,12:hello world!,')

	s = stons(stons('name') + stons('value'))
	print 'Created embedded: "%s"' % s
	print '  Contains: "%s"' % nstos(s)
	s2 = nstos(s)
	while 1:
		if not s2: break
		s2, rest = nstos(s2, 1)
		print '    s2: "%s"' % s2
		print '      rest: "%s"' % rest
		s2 = rest
'''

revision = '$Revision: 1.14 $'

import string
import socket, select
import types
import os


class LengthOverflow:
	'''Exception class thrown when a netstring is larger than the
	user requested it be.'''
	def __init__(self, s): self.data = s
	def __str__(self): return(self.data)


def nstos(s, returnRemainder = 0):
	'''Convert a netstring into a regular string (possibly returning remainder).

	RETURNS: Parse netstring into regular string and return it.  If
		"returnRemainder" is true, a tuple of ( string, remainder ).
	EXCEPTIONS: ValueError is raised if argument is not a valid netstring.
	ARGUMENTS:
		- s -- string to parse into a regular string.
		- returnRemainder -- If true, a tuple is returned consisting of
			the parsed netstring, and the unparsed portion of the string.
			The remainder is empty if the entire string was consumed.
	'''
	colonpos = string.find(s, ':')
	if colonpos < 1: raise ValueError, 'Invalid format for netstring'
	try: size = int(s[:colonpos])
	except: raise ValueError, 'Left hand side of ":" must be only numeric'

	strstart = colonpos + 1
	termpos = strstart + size
	if len(s) - 1 < termpos:
		raise ValueError, 'Expected string of length %d, got %d' % ( termpos + 1,
				len(s) )
	if s[termpos] != ',':
		raise ValueError, 'Netstring must be terminated by a comma.'

	rawstr = s[strstart:termpos]
	if returnRemainder:
		remainder = s[termpos + 1:]
		return(( rawstr, remainder ))
	else:
		return(rawstr)

def stons(s):
	'''Convert a regular string into a netstring.

	RETURNS: Return argument converted into a netstring.
	EXCEPTIONS: None generated internally.
	ARGUMENTS:
		- s -- string to convert into a netstring.  If 's' is a list/tuple, each
		       element in turn is converted to a netstring, and the results are
				 concatenated and turned into a netstring.
	'''
	if type(s) == types.ListType:
		s2 = ''
		for s in s: s2 = s2 + stons(s)
		return(stons(s2))
	if type(s) == types.TupleType:
		s2 = ''
		for s in s: s2 = s2 + stons(s)
		return(stons(s2))

	return('%d:%s,' % ( len(s), s ))


################
def nstolist(s):
	'''Convert a netstring into a list.
	Given the netstring created by stons() when passed a list, this returns
	the original list.

	RETURNS: Return argument converted into a list.
	EXCEPTIONS: None generated internally.
	ARGUMENTS:
		- s -- netstring to convert to a list.
	'''
	s = nstos(s)
	list = []
	while s:
		e, s = nstos(s, returnRemainder = 1)
		list.append(e)
	return(list)


###################
def dicttons(dict):
	'''Convert a dictionary into a netstring.
	Given a dictionary, convert it into a netstring consisting of
	netstrings for each key/value pair, where each is converted
	using "str()".

	RETURNS: Argument converted into a netstring.
	EXCEPTIONS: None generated internally.
	ARGUMENTS:
		- dict -- dictionary to convert into a netstring.
	'''
	s = ''
	keys = dict.keys()
	keys.sort()
	for key in keys:
		s = s + stons(str(key)) + stons(str(dict[key]))
	return(stons(s))


#####################################
def nstodict(s, returnRemainder = 0):
	'''Convert a dictionary into a netstring.
	Given the netstring created by dicttons(), this converts it back into
	a dictionary.  All keys and values are strings, no matter their
	original type in the dictionary.

	RETURNS: Argument converted into a dictionary.
	EXCEPTIONS: ValueError -- If string doesn't contain an even number of
		sub-netstrings.
	ARGUMENTS:
		- s -- netstring to convert into a dictionary.
	'''
	dict = {}

	remainder = ''
	if returnRemainder:
		s, remainder = nstos(s, returnRemainder = 1)
	else:
		s = nstos(s)

	while s:
		try:
			key, s = nstos(s, returnRemainder = 1)
			value, s = nstos(s, returnRemainder = 1)
		except ValueError:
			raise ValueError, 'Argument must have an even number ' \
					'of sub-netstrings.'
		dict[key] = value

	if returnRemainder:
		return(( dict, remainder ))
	else:
		return(dict)


##################
def iscomplete(s):
	'''Return 1 if there are enough bytes to construct a netstring.

	RETURNS: 1 if there are enough bytes in argument to construct a netstring.
		For example if argument is "12:hello world!" (or shorter) this function
		will return 0, while if it's "12:hello world!," (or longer) it will
		return 1.
	EXCEPTIONS: None generated internally.
	ARGUMENTS:
		- s -- string to check to for completeness.
	'''
	colonpos = string.find(s, ':')
	if colonpos < 1: return 0
	try: size = int(s[:colonpos])
	except: raise ValueError, 'Left hand side of ":" must be only numeric'
	return(len(s) > size + colonpos + 1)


################################
def sockread(sock, bytesToRead):
	data = ''
	while bytesToRead > 0:
		rfdl = select.select([sock] , [], [], None)[0]
		if len(rfdl) == 0: return(data)
		s = rfdl[0].recv(bytesToRead)
		if not s: raise IOError, 'Error reading netstring data.'
		bytesToRead = bytesToRead - len(s)
		data = data + s
	return(data)


#############
class Reader:
	#######################################
	def __init__(self, src, maxLen = None):
		self.src = src
		if hasattr(self.src, 'read'): self._get = self.src.read
		elif hasattr(self.src, 'recv'): self._get = self.src.recv
		elif type(self.src) == types.IntType:
			self._get = lambda n, fd = self.src: os.read(fd, n)
		else: raise TypeError, 'Argument must have recv() or read() methods.'
		self.maxLen = maxLen
		self.len = 0
		self.startnew()


	##################################
	def startnew(self, maxLen = None):
		if not maxLen: maxLen = self.maxLen
		data = ''

		#  consume existing data in current netstring
		while self.len > 0: self.read(4096)

		self.len = 0
		while 1:
			s = self._get(1)
			if not s:
				raise ValueError, 'Ran out of data reading netstring header.'
			if s == ':':
				self.len = int(data)
				break
			if not s in string.digits:
				raise ValueError, 'Input stream had invalid character "%s"' % s
			data = data + s
			if maxLen and data and int(data) > maxLen or len(data) > maxLen:
				raise LengthOverflow, 'Netstring larger than requested limit.'


	##########################
	def read(self, toread = None):
		if toread == None or toread > self.len: toread = self.len
		if toread < 1: return('')
		data = self._get(toread)
		self.len = self.len - len(data)
		if self.len == 0:
			s = self._get(1)
			if s != ',':
				raise ValueError, 'Found character "%s" instead of ","' % s
		return(data)
	

	############################
	def __getattr__(self, attr):
		return(getattr(self.src, attr))


#class netstringSocket:
#	def __init__(self, sock):
#		self.sock = sock
#
#	def __getattr__(self, name):
#		attr = getattr(self.sock, name)
#		setattr(self, name, attr)
#		return(attr)
#
#socket(family, type, proto = 0):
#	return(netstringSocket(socket.socket(family, type, proto)))
#
#fromfd(fd, family, type, proto = 0):
#	return(netstringSocket(socket.fromfd(fd, family, type, proto)))

if __name__ == '__main__':
	s = stons(stons('name') + stons('value'))
	print 'Created embedded: "%s"' % s
	print '  Contains: "%s"' % nstos(s)
	s2 = nstos(s)
	while 1:
		if not s2: break
		s2, rest = nstos(s2, 1)
		print '    s2: "%s"' % s2
		print '      rest: "%s"' % rest
		s2 = rest
	print 'Doing hello+there "%s"' % str(nstos('5:hello,5:there,', 1))
	print 'Doing empty+there "%s"' % str(nstos('0:,5:there,', 1))
	print 'Creating hello world!: "%s"' % stons('hello world!')
	print 'Iscomplete:', iscomplete('12:hello world')
	print 'Iscomplete:', iscomplete('12:hello world!')
	print 'Iscomplete:', iscomplete('12:hello world!,')
	print 'Iscomplete:', iscomplete('12:hello world!,12:hello world!,')
	s = dicttons({ 'a' : 1, 'b' : 'abc', 'c' : 123 })
	print 'dicttons:', s
	print 'nstodict:', nstodict(s)
